Urban Network Analysus (UNA) Tools

Example 1: Setting up a Network and Measuring Pedestrian Accissibility

Learning Objectives

In this example, we will learn how to
  • Get comfortable with the setup
  • Loading data, handling layers and visualizing output
  • Creating network from a layer, inserting origins and destinations, creating a network.
  • Run basic accissibility measures: Reach and Gravity
  • Use the service area and the closest destination UNA tools
  • Getting comfortable with the setup

    In order to use the UNA tools in Python, we use a spatial analysis library called Madina (Arabic for city). a Madina object holds data in a manner similar to the layering system used in GIS and Rhino. to use Madina, we first create an object and start loading data layers onto it.
    In [ ]:
    import sys
    sys.path.append('../')
    from madina.zonal.zonal import Zonal
    import madina.una.tools as una
    
    cambridge = Zonal()
    

    Since there is no GUI ()graphical user interface), whenever you need to know details about your Zonal object, call the describe() function

    In [ ]:
    cambridge.describe()
    
    No zonal_layers yet, load a layer using 'load_layer(layer_name, file_path)'
    Geographic center: (None, None)
    No network graph yet. First, insert a layer that contains network segments (streets, sidewalks, ..) and call create_street_network(layer_name,  weight_attribute=None)
    	Then,  insert origins and destinations using 'insert_nodes(label, layer_name, weight_attribute)'
    	Finally, when done, create a network by calling 'create_street_network()'
    

    Loading Data Layers

    Just like GIS and Rhino, the way to incorporate data into your analysis is through loading data in a layering system. The library supports different formats like .geoJSON and .shp. To load a layer, call the function load_layer() giving it a layer name for your new layer, and a path to where the data is stored.
    In [ ]:
    cambridge.load_layer(
        layer_name="building_entrances",
        file_path="./Data/building_entrances.geojson"
    )
    cambridge.load_layer(
        layer_name="subway",
        file_path="./Data/subway.geojson"
    )
    cambridge.load_layer(
        layer_name="sidewalks",
        file_path="./Data/sidewalks.geojson"
    )
    

    Looking inside

    There are two ways to look at your data:
    • Visual Map: creating a map is easy. Just call the .create_deck_map()function. to save the map as a file, call the same function, with a save_as=cambridge_map.html". Colors are set randomly, but we'll go over map styling in a later lab. The current map visualizations utilize a library called PyDeck, a python wrapper for the javascript library Deck.GL open-sourced by a major ride-hailing provider that's based on the WebGL powertful 3D visualization framework made by a non-profit organization. Take this moment to appreciate the value of open source work here that made this possible (and free) and consider pushing for more open sourced projects in your future life.
    • Text: This would come handy when you need to know some details about your layers. Call the function cambridge.describe() to get a summary of your layers, their current settings and what columns are in each layer. This would be very handy when you're trying to debug for errors, especially when you start using data from external sources that might have different naming conventions, or when you're trying to deal with coordinate reference systems or projection systems.
    In [ ]:
    cambridge.create_map()
    
    Out[ ]:

    You can easily specify a color for each layer:

    In [ ]:
    cambridge.create_map(
        [
            {'layer': 'sidewalks', 'color': [125, 125, 125]}, 
            {'layer': 'subway', 'color': [255, 0, 0]}, 
            {'layer': 'building_entrances', 'color': [0, 0, 255]}
        ]
    )
    
    Out[ ]:
    In [ ]:
    cambridge.describe()
    
    Layer name           | Visible | projection | rows  | File path           
    building_entrances   |       1 | EPSG:3857  |   118 | ./Data/building_entrances.geojson
    subway               |       1 | EPSG:3857  |     2 | ./Data/subway.geojson
    sidewalks            |       1 | EPSG:3857  |   170 | ./Data/sidewalks.geojson
    Geographic center: (-0.014369793082880312, 0.0016430747641603674)
    No network graph yet. First, insert a layer that contains network segments (streets, sidewalks, ..) and call create_street_network(layer_name,  weight_attribute=None)
    	Then,  insert origins and destinations using 'insert_nodes(label, layer_name, weight_attribute)'
    	Finally, when done, create a network by calling 'create_street_network()'
    

    Creating a network

    Networks (i.e. graphs) are a groups of nodes (i.e. vertices) and edges. The sidewalk network we loaded earlier might look graphically like a network, but for it to be useful, we need to convert it to a topologically connected sets of nodes and edges. Calling the create_network_nodes_edges() function does exactly that, we just need to give it a parameter source_layer="sidewalks" to let it know we're constructing a network from the sidewalk layer. When called, this function create two layers internally: network_nodes and network_edges. the network_edges table is equivelant to what's known as an edge list in graph theory.
    In [ ]:
    cambridge.create_street_network(source_layer="sidewalks")
    

    to see what the network looks like, we can call the cambridge.create_map() giving it a list of the layers we want to see.

    In [ ]:
    cambridge.create_map(
        layer_list=[
            {'gdf': cambridge.network.nodes, 'color': [255, 0, 255]}, 
            {'gdf': cambridge.network.edges, 'color': [125, 125, 125]}
        ]
    )
    
    Out[ ]:

    Inserting Origins and Destinations

    In urban networks, we will use the terms origins and destinations frequently. Origins are where trips are generated, and destinations are where trips are distributed to. These are the first two steps of the Four-step model for traffic simulation. The third step is mode choice, which for the purposes of this library, is assumed to all be pedestrian trips. The fourth part is route assignment which we will learn more about when we talk about a generalization of the betweenness algorithm for route assignment.
    In [ ]:
    # inserting origins:
    cambridge.insert_node(
        label='origin',
        layer_name="subway",
    )
    
    # inserting destinations
    cambridge.insert_node(
        label='destination',
        layer_name="building_entrances",
    )
    

    As a convention in this library, there are a few default colors when visualizing the network_nodes and network_edges layers:

    • Origins are blue
    • Destinations are red
    • Network edges (road segments, sidewalk segments,...) are shown in a dark gray for intersection nodes, and lighter gray for network segments
    • If you don't see an intersection node when you expect people being able to from an edge to another one, the network is not topologically correct, and some possible routes would be missed. in the Rhino part A of this excercise, we learned how to clean up a network. Whem working with this library, we assume the input to create_network_nodes_edges() is a clean geometry where the end of a line exactly touches the begenning of another one to establish a connection.
    In [ ]:
    cambridge.create_map(
        layer_list=[
            {'gdf': cambridge.network.nodes, 'color_by_attribute': 'type', 'color_method': 'categorical'}, 
            {'gdf': cambridge.network.edges, 'color': [125, 125, 125]}
        ]
    )
    
    Out[ ]:

    Constructing a graph

    The last step before being able to do an analysis, is to commit to a finalized graph. once you're done creating your network, and inserted your origins and destinations, you can call the create_graph() function that creates a "graph" object for your analysis.
    In [ ]:
    cambridge.create_graph()
    

    Analyze how many building entrances or people can be reached from subway stations in a 5-minute (300m) walkshed.

    From now on, we will attempt doing the same analysis done on the Rhino part B of this exercise. THe following steps are identical to the Rhino part, and the same intuition and mathematical representation still holds.

    Reach Index

    Running the una_accessibility and giving it parameters reach=True, search_radius=300 will measure how many how many destinations (building entrances) are reachable from origins (subway).

    We see that the northen station could be reached by 106 building entrance, while the southern station could be reached by 112 building entrance.

    The una_accessibility creates a column called una_reach when the parameter reach=True. We could use that to explore more visualization functionalities:

    In [ ]:
    una.accessibility(
        cambridge,
        reach=True,
        search_radius=300
    )
    
    
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes, "radius": "una_reach", 'text':'una_reach', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:

    Setting the parameter weight='people' enables us to weight the destinations (building entrances) by how many people actually live in these building. We could then see that 2,789 people could reach the northen station by walking a maximum of 300 meters. The southern station could be reached by 3,017 people.

    In [ ]:
    una.accessibility(
        cambridge,
        reach=True,
        search_radius=300,
        weight='people'
    )
    
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes, "radius": "una_reach", 'text':'una_reach', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:

    Service Area

    Sometimes, we want to examine a section of our data in isolation. The service_area() function is a great introduction into that. The library internally uses a library called GeoPandas, where a layer is usually stored as a GeoDataFrame. GeoPandas are an extenstion to the well known Pandas library. If you're familiar with the Pandas Dataframe, you'll be able to use all what you know about a Dataframe. The only difference is that a GeoDataFrame always has a geometry columns, and supports a wide array of spatial operations.

    The service_area() function returns a GeoDataFrame destinations containing all the destinations covered by an origin's service area, and a GeoDataframe network_edges containing all network segments inside the origin's service area. A Third Dataframe scope_gdf contains the boundaries of the service area. Notice how the function create_deck_map() could take either a layer from the layers we loaded, or a gdf (i.e. GeoDataFrame) resulting from a process.

    In [ ]:
    destinations, network_edges, scope_gdf = una.service_area(
        cambridge,
        #[120],
        search_radius=100,
    )
    
    cambridge.create_map(
        layer_list=[
            {"layer": 'sidewalks'},
            {"layer": 'building_entrances'},
            {"gdf": network_edges, "color": [0, 255, 0]},
            {"gdf": destinations, "color": [255, 0, 0]},
            {"gdf": scope_gdf[scope_gdf['name'] == 'service area border'], "color": [0, 0, 255], 'opacity': 0.10},
        ]
    )
    
    Out[ ]:

    Setting up Attributes

    Generally, in a coding environment, you would rarely need to change or set one single attribute of one single element in a layer. Attributes are usually either pure data inputs, or a calculated result that is not set manually. In this contexct, setting up one single attribute might be really valuble when you are debugging your work, want to validate the math/logic or just want to experament with specific values. The set_attribute() function allows for that.

    While in a visual interface, it is possible to select the elements you want with a mouse. In a coding environment, every object have an identifier, and each layer once loaded is assigned an id attribute. Hover over tha previous map to find the building entrances with ids 2 and 115, They are the same one referref to in the Rhino bullet point 19. They are both in layer="building_entrances and we want to set a attribute='student' to values 10 and 10, for ids 2 and 115 respectively.

    From the map below, we now notice that 20 students can reach the northen station, and 10 students could reach he southern station on a 300 meter walk.

    In [ ]:
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes.reset_index(), 'text':'id', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:
    In [ ]:
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes.loc[[2, 115]].reset_index(), 'text':'id', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:
    In [ ]:
    cambridge.layers['building_entrances'].gdf.at[2, 'students'] = 10
    cambridge.layers['building_entrances'].gdf.at[115, 'students'] = 10
    
    
    
    una.accessibility(
        cambridge, 
        reach=True,
        search_radius=300,
        weight='students'
    )
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes, "radius": "una_reach", 'text':'una_reach', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:

    Gravity Index

    The gravity model of transportation is one of many distance decay functions. The aim of a distance decay function (e.g. gravity) is to quantify the inverse relationship between distance, and willingness to make trips. applying gravity peanilize further destinations by reducing their contribution, assume that they are less accissable. For this reason, a gravity index is always less than or equal to a reach index.

    When factoring in a gravity decay, we notice that the northen station only attracts 16.88 students (As opposed to 20) and the southern station now attracts only 8.67 students (as opposed to 10).

    In [ ]:
    una.accessibility(
        cambridge, 
        gravity=True,
        search_radius=300,
        beta=0.001,
        alpha=1,
        weight='students',
    )
    
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes, "radius": "una_gravity", 'text':'una_gravity', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:

    Comparing Reach and Gravity of People to Subway

    We go back to using people as weight, and we again call the function una_accessibility(), this time, setting both reach and gravity to True

    Hovering over the map below to where the stations are, we could see that the northern station has a reach index of 2,789 bur a gravity index of only 2,361.54, The southern station has a reach index of 3,017 and a gravity_index of only 2,606.81.

    In [ ]:
    una.accessibility(
        cambridge, 
        reach=True,
        gravity=True,
        search_radius=300,
        beta=0.001,
        alpha=1,
        weight='people',
    )
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes, "radius": "una_gravity", 'text':'una_gravity', 'color': [255, 0, 0]},
        ]
    )
    
    Out[ ]:

    Closest Facility

    One note about the una_accessibility(), there will be overlap between destinations assigned to each origin, so a destination would be double counted in an origin's accissability indices. The closest_facility() assigns each destination (in this case, building entrance) to one single origin (a subway station in this case)

    Function una.accessibility() una.closest_facility()
    Station Reach Graviy Reach Gravity
    Northren 2,789.00 2.361.54 1,265.00 1,120.92
    Southren 3,017.00 2,606.81 1,914.00 1,716.57

    This variation of values (across the same station), poses the question of What value is closest to reality? This is an ongoing research topic, and urban scientists are able to choose better models the more we learn about pedestrian activity through data,

    In [ ]:
    una.closest_facility(
        cambridge,
        weight='people',
        gravity=True,
        reach=True,
        search_radius=300,
        beta=0.001
    )
    
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]},
            {"gdf": cambridge.network.nodes,"text": "una_closest_destination_distance",  "color_by_attribute": "una_closest_destination", "color_method": "categorical"},#, 'radius': "una_reach"},
        ]
    )
    
    Out[ ]:

    What About the Reverse? Reach and Gravity of Subway to People

    To examine the different interpretation of accessibility indices based on what we define as origins and destinations, lets flip our origins from subway to building_entrances and destinations from building_entrances to subway.

    In order to do this, we need to first clear up the origins and destination nodes we created earlier, bur we could still use the same neteowk edges (sidewalks) as before, since changing the origins and destinations will not impact the underlying network segments and intersections. Calling the function clear_nodes() empties out our existing oeigins and destinations. We then need to insert origins and destinations, then create a graph object, just like before.

    In [ ]:
    cambridge.clear_nodes()
    cambridge.insert_node(
        label='origin',
        layer_name="building_entrances",
    )
    cambridge.insert_node(
        label='destination',
        layer_name="subway",
    )
    cambridge.create_graph()
    

    The reach index shown below, shows how many subway station could be reached from each building enterance, by walking 300 meters. Most building entrances can access two stations, but buildings on the phrepherey, could only access one of the two stations within a 200 meter walk,

    In [ ]:
    una.accessibility(
        cambridge, 
        reach=True,
        search_radius=300,
    )
    
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.nodes, "color_by_attribute": "una_reach", 'color_method': 'gradient',  'text':'una_reach'},
            {"gdf": cambridge.network.edges, 'color': [125, 125, 125]}
        ]
    )
    
    Out[ ]:

    As for the gravity index, the interpretation of what these values mean is sensitive to the bets parameter which could translate to the "area's willingness to walk". living in a building with a high gravity index, means that you have more subway options that are close and could easily be reached on foot. A low gravity index, means that you live in a building with fewer subway options that are further out.

    In [ ]:
    una.accessibility(
        cambridge, 
        reach=True,
        gravity=True,
        search_radius=300,
        beta=0.004,
        alpha=1,
    )
    cambridge.create_map(
        layer_list=[
            {"gdf": cambridge.network.nodes, "color_by_attribute": "una_gravity", 'color_method':'gradient', 'text': 'una_gravity'},
            {"gdf": cambridge.network.edges, 'color': [135, 125, 125]}
        ]
    )
    
    Out[ ]:
    In [ ]:
     
    
    In [ ]:
     
    

    TODO¶

    Network map¶

    • fix network visualization, nodes and edges are not colored right
    • fix network visualization, show node/edge as attribute
    • add network snapping line as part of create map. bring from debugger code.

    UNA tools¶

    • add accissibility tool, check behaviour
    • add service area tool, check behavioour
    • add redundant paths tool, check behaviour